<?php
/**
 * @package     CiMS.framework
 *
 * @copyright   Copyright (C) 2012 Cimaron Shanahan. All rights reserved.
 * @license     http://opensource.org/licenses/mit-license.php MIT License
 */
defined('CiMS') or die;


/**
 * Base Object Class
 */
class CiMSObject {

	protected $_data = array();

	/**
	 * Get instance of object
	 *
	 * @param   array   $data   Unique config data
	 *
	 * @return  object
	 */
	public static function getInstance($data = array()) {
		$instances = array();

		if (version_compare(PHP_VERSION, '5.3')) {
			$class = get_called_class();
		} else {
			$trace = debug_backtrace();
			$class = $trace[1]['class'];
		}

		if (!isset($instances[$class])) {
			$instances[$class] = array();
		}

		$uid = md5(serialize($data));
		if (!isset($instances[$class][$uid])) {
			$instances[$class][$uid] = new $class($config);
		}

		return $instances[$class][$uid];
	}

	/**
	 * Constructor
	 *
	 * @param   array   $data   Data to initialize
	 */
	public function __construct($data = array()) {
		if (!empty($data)) {
			$this->set($data);
		}
	}

	/**
	 * Getter
	 *
	 * @param   string   $key   Key
	 *
	 * @return  mixed
	 */
	public function __get($key) {
		return $this->get($key);
	}

	/**
	 * Setter
	 *
	 * @param   string   $key     Key
	 * @param   mixed    $value   Value
	 */
	public function __set($key, $value) {
		$this->set($key, $value);
	}

	/**
	 * Get value or values
	 *
	 * @param   mixed   $key       Key, or null to return all data
	 * @param   mixed   $default   Default value if not present
	 *
	 * @return  mixed
	 */
	public function get($key = NULL, $default = NULL) {

		if (is_null($key)) {
			return $this->_data;
		}

		if (isset($this->_data[$key])) {
			return $this->_data[$key];
		}

		return $default;
	}

	/**
	 * Set value or values
	 *
	 * @param   mixed   $key     Key, or array of key => values
	 * @param   mixed   $value   Value (optional)
	 */
	public function set($key, $value = NULL) {
		if (is_array($key)) {
			foreach ($key as $k => $v) {
				$this->_data[$k] = $v;
			}
		} else {
			$this->_data[$key] = $value;
		}
	}

	/**
	 * Set data as a reference to passed array
	 *
	 * @param   array   $data   Data
	 */
	public function setRef(&$data) {
		$this->_data = $data;
	}

	/**
	 * Has value
	 *
	 * @param   string   $key     Key
	 *
	 * @return  bool	 
	 */
	public function has($key) {
		return isset($this->_data[$key]);
	}

	/**
	 * Clear data
	 *
	 * @param   string   $key   Key
	 */
	public function clear($key) {
		unset($this->_data[$key]);
	}

	/**
	 * Reset data
	 */
	public function reset() {
		$this->_data = array();
	}

	/**
	 * Get by path
	 *
	 * @param   string   $path      Path
	 * @param   mixed    $default   Default value if not present
	 *
	 * @return  mixed
	 */
	public function getByPath($path, $default = NULL) {
		return self::getByPathNode($this, $path, $default);
	}

	/**
	 * Set by path
	 *
	 * @param   string   $path    Path
	 * @param   mixed    $value   Value
	 */
	public function setByPath($path, $value) {
		return self::setByPathNode($this, $path, $value);
	}

	/**
	 * Get by path from node
	 *
	 * @param   mixed    $node      Node
	 * @param   string   $path      Path
	 * @param   mixed    $default   Default value if not present
	 *
	 * @return  mixed
	 */
	public static function getByPathNode(&$node, $path, $default = NULL) {
		$path = explode('/', $path);
		$key = array_shift($path);
		$path = implode('/', $path);

		//End case
		if (empty($key)) {
			return $node;
		}

		//Not an array or object
		if (!is_object($node) && !is_array($node)) {
			return $default;
		}

		//Try to get array key
		if (is_array($node)) {
			if (isset($node[$key])) {
				$newnode = $node[$key];
			} else {
				return $default;
			}
		}

		//Try to get object key
		if (is_object($node)) {
			if (method_exists($node, 'get')) {
				$newnode = $node->get($key, $default);
			} elseif (isset($node->$key)) {
				$newnode = $node->$key;
			} else {
				return $default;
			}
		}

		return self::getByPathNode($newnode, $path, $default);
	}

	/**
	 * Set by path from node
	 *
	 * @param   mixed    $node    Node
	 * @param   string   $path    Path
	 * @param   mixed    $value   Value
	 *
	 * @return  bool
	 */
	public static function setByPathNode(&$node, $path, $value = NULL) {
		$path = explode('/', $path);
		$key = array_shift($path);
		$path = implode('/', $path);

		//End case
		if (empty($key)) {
			return false;
		}

		//Not an array or object
		if (!is_object($node) && !is_array($node)) {
			return false;
		}

		//Try to get array key
		if (is_array($node)) {

			//End of road, we need to set
			if (empty($path)) {
				$node[$key] = $value;
				return true;
			}

			if (!isset($node[$key])) {
				$node[$key] = array();
			}

			$newnode = $node[$key];
		}

		//Try to get object key
		if (is_object($node)) {
		
			//End of road, we need to set
			if (empty($path)) {
				if (method_exists($node, 'set')) {
					$node->set($key, $value);
					return true;
				} else {
					$node->$key = $value;
					return true;
				}
			}

			if (method_exists($node, 'get')) {
				$newnode = $node->get($key);
				if (is_null($newnode)) {
					$newnode = new CiMSObject();
					$node->set($key, $newnode);
				}
			} else {
				if (!isset($node->$key)) {
					$newnode = new stdClass;
					$node->$key = $newnode;
				} else {
					$newnode = $node->$key;
				}
			}
		}

		return self::setByPathNode($newnode, $path, $value);
	}

	/**
	 * To String
	 *
	 * @param   bool   $html       Pretty formatting for html
	 * @param   int    $tablevel   Tabulation level
	 *
	 * @return  string
	 */
	public function toString($html = false, $tablevel = 0, $stack = array()) {
		$out = get_class($this) . " {\n";
		$tab = str_repeat(' ', 4);
		$padding = str_repeat($tab, $tablevel);

		foreach ($this->_data as $key => $value) {
			if (in_array($this, $stack, true)) {
				$pos = array_search($this, $stack, true);
				$printed = '*RECURSION* @'.$pos;
			} elseif (method_exists($value, 'toString')) {
				$printed = $value->toString(false, $tablevel + 1, array_merge($stack, array($this)));
			} else {
				$printed = print_r($value, true);
				$printed = str_replace("\n", "\n$tab".$padding, $printed);
			}
			$out .= "$tab{$padding}[$key] => $printed\n";
		}

		$out .= $padding."}\n";
		
		if ($html) {
			$out = '<pre>'.htmlentities($out).'</pre>';
		}
		
		return $out;
	}
	
	/**
	 * To String magic method
	 *
	 * @return  string
	 */
	public function __toString() {
		return $this->toString();
	}
}

